package hydrap2p.proxy.manager;

import hydrap2p.ServiceLocation;
import hydrap2p.logging.Level;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import org.apache.thrift.TException;

public class ManagerConnectionManager {
	private Map<Integer, ManagerConnectionProcessor> connectionmap;
	
	private static ManagerConnectionManager instance;

	private ManagerConnectionManager()
	{
		connectionmap=new TreeMap<Integer, ManagerConnectionProcessor>();
	}
	public static ManagerConnectionManager create()
	{
		instance=new ManagerConnectionManager();
		return instance;
	}
	
	private ManagerConnectionManager(int port) throws IOException
	{
		connectionmap=new TreeMap<Integer, ManagerConnectionProcessor>();
		ManagerConnectionListener listener=new ManagerConnectionListener(port);
		new Thread(listener,listener.toString()).start();
	}
	public static ManagerConnectionManager create(int port) throws IOException
	{
		instance=new ManagerConnectionManager(port);
		return instance;
	}
	
	public static ManagerConnectionManager getInstance()
	{
		return instance;
	}
	
	public void startConnections(Configuration config)
	{
		Map<ServiceLocation, ManagerConnectionProcessor> map=new TreeMap<ServiceLocation, ManagerConnectionProcessor>();
		while (true)
		{
			List<ServiceLocation> notconnected=new LinkedList<ServiceLocation>();
			for (ServiceLocation peer:config.getPeers())
			{
				if (map.containsKey(peer))
				{
					if (!map.get(peer).isRunning())
					{
						map.remove(peer);
					}
				}
				if (!map.containsKey(peer))
					notconnected.add(peer);
			}
			for (ServiceLocation peer:notconnected)
			{
				ManagerConnectionProcessor temp;
				try {
					System.err.println("Connecting to peer "+peer);
					temp=connect(peer);
					map.put(peer, temp);
				} catch (IOException e) {
					// Could not connect to peer
					System.err.println("Could not connect to peer "+peer+", trying again soon");
				}
			}
			try {
				Thread.sleep(3000);
			} catch (InterruptedException e) {
				return;
			}
		}
	}
	
	/**
	 * Specifies that the given peerid is handled by the given processor
	 * @param peerid
	 * @param processor
	 */
	public void setPeerID(Integer peerid, ManagerConnectionProcessor processor) throws IllegalArgumentException
	{
		//ProxyLogging.log(Level.DEBUG, "ProxyManager - ManagerConnectionManager", "Connection successful from PeerID "+peerid);

		synchronized(connectionmap)
		{
			if (connectionmap.containsKey(peerid))
				throw new IllegalArgumentException("Peer ID already connected");
			connectionmap.put(peerid, processor);
		}
		if (Main.ManagerPort>0)
			sendManagerLocation(new ServiceLocation("127.0.0.1",Main.ManagerPort));
	}
	
	public void remove(Integer peerid, ManagerConnectionProcessor processor)
	{
		//ProxyLogging.log(Level.DEBUG, "ProxyManager - ManagerConnectionManager", "Removing connection from PeerID "+peerid);
		synchronized(connectionmap)
		{
			if (peerid!=null)
			{
				connectionmap.remove(peerid);
				ServiceTunnels.getInstance().removePeerTunnels(peerid);
				CallbackManager.getInstance().closePeerCallbacks(peerid);
			}
		}
	}
	
	public ManagerConnectionProcessor connect(ServiceLocation location) throws IOException
	{
		Socket temp=new Socket(location.address, location.port);
		ManagerConnectionProcessor ret=new ManagerConnectionProcessor(temp);
		new Thread(ret,ret.toString()).start();
		return ret;
	}
	
	public void sendManagerLocation(ServiceLocation manager)
	{
		//ProxyLogging.log(Level.DEBUG, "ProxyManager - ManagerConnectionManager", "Broadcasting found manager location "+manager);
		List<ManagerConnectionProcessor> connections;
		synchronized(this.connectionmap)
		{
			connections=new LinkedList<ManagerConnectionProcessor>(connectionmap.values());
		}
		for (ManagerConnectionProcessor connection:connections)
		{
			connection.sendManagerLocation(manager);
		}
	}
	
	public ServiceLocation openCallbackPort(ServiceLocation local, int peerid) throws TException
	{
		//ProxyLogging.log(Level.DEBUG, "ProxyManager - ManagerConnectionManager", "Requesting callback port to "+local+" peerid="+peerid);
		try {
			ManagerConnectionProcessor connection=null;
			synchronized(connectionmap)
			{
				if (connectionmap.containsKey(peerid))
				{
					connection=connectionmap.get(peerid);
				}
			}
			if (connection!=null)
			{
				ProxyLogging.log(Level.DEBUG, "ProxyManager - ManagerConnectionManager", "Starting new connection");
				NetworkOutstandingCallbacks.prepareFor(local);
				connection.openCallbackPort(local);
				ServiceLocation ret=NetworkOutstandingCallbacks.waitFor(local);
				if (ret!=null)
				{
					ProxyLogging.log(Level.DEBUG, "ProxyManager - ManagerConnectionManager", "Successful connection: "+ret);
					return ret;
				}
				ProxyLogging.log(Level.ERROR, "ProxyManager - ManagerConnectionManager", "Failed to open callback port");
				throw new TException("Could not open callback port");
			}
		} catch (IOException e) {
			// The remote peer was disconnected
			ProxyLogging.log(Level.ERROR, "ProxyManager - ManagerConnectionManager", "Failed to communicate with remote peer");
			throw new TException("Could not communicate with remote peer");
		}
		return null;
	}
	
	public ServiceLocation openCallbackPort(ServiceLocation local, ServiceLocation target) throws TException
	{
		try {
			//ProxyLogging.log(Level.DEBUG, "ProxyManager - ManagerConnectionManager", "Opening callback by target location: "+target);
			int peerid=ServiceTunnels.getInstance().findPeerID(target.port);
			return openCallbackPort(local, peerid);
		}
		catch (IllegalArgumentException e)
		{
			// Apparently, the desired target port can not be found
			//ProxyLogging.log(Level.ERROR, "ProxyManager - ManagerConnectionManager", "Could not locate peer with target ServiceLocation: "+target);
			throw new TException("Could not locate peer with target ServiceLocation: "+target);
		}
	}
	
	public void sendData(int peerid, ServiceLocation from, ServiceLocation to, byte[] data, int size) throws TException
	{
		try {
			//ProxyLogging.log(Level.DEBUG, "ProxyManager - ManagerConnectionManager", "Sending "+size+" data from "+from+" to "+to);
			ManagerConnectionProcessor connection=null;
			synchronized(connectionmap)
			{
				if (connectionmap.containsKey(peerid))
					connection=connectionmap.get(peerid);
			}
			if (connection!=null)
			{
				connection.sendData(from, to, data, size);
			}
		} catch (IOException e) {
			// The remote peer was disconnected
			//ProxyLogging.log(Level.ERROR, "ProxyManager - ManagerConnectionManager", "Failed to communicate with remote peer");
			throw new TException("Could not communicate with remote peer");
		}
	}
}
